package tools.httptools;

import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.Value;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Validate;
import org.apache.http.HttpEntity;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Content;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Form;
import org.apache.http.client.fluent.Request;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.message.BasicHeader;
import org.apache.http.ssl.SSLContexts;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import tools.cookietools.ReadOnlyCookie;
import tools.cookietools.ReadOnlyCookieStore;
import tools.streamtools.NullableStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.CR;
import static org.apache.commons.lang3.StringUtils.LF;
import static org.apache.commons.lang3.StringUtils.isNoneBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.http.client.fluent.Content.NO_CONTENT;
import static org.apache.http.client.fluent.Request.Get;
import static org.apache.http.client.fluent.Request.Post;
import static org.apache.http.client.fluent.Request.Put;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;
import static org.apache.http.entity.ContentType.MULTIPART_FORM_DATA;
import static org.apache.http.entity.ContentType.TEXT_PLAIN;
import static org.apache.http.entity.mime.HttpMultipartMode.BROWSER_COMPATIBLE;

@Slf4j
public class HttpClientUtils {

    //region helper
    public static String doGet(String url) {
        return get(url).executeString();
    }
    //endregion


    //region builder
    public static RequestBuilder get(String url, Map<String, String> queryMap) {
        url = UriComponentsBuilder.fromHttpUrl(url)
            .queryParams(toMultiValueMap(queryMap))
            .build()
            .toUriString();
        return get(url);
    }

    public static RequestBuilder get(String url) {
        return new RequestBuilder(Get(url));
    }

    public static RequestBuilder put(String url) {
        return new RequestBuilder(Put(url));
    }

    public static RequestBuilder post(String url) {
        return new RequestBuilder(Post(url));
    }

    public static RequestBuilder post(String url, Map<String, String> queryMap) {
        url = UriComponentsBuilder.fromHttpUrl(url)
            .queryParams(toMultiValueMap(queryMap))
            .build()
            .toUriString();
        return post(url);
    }

    public static class RequestBuilder {
        private static final AtomicLong requestIdGenerator = new AtomicLong(currentTimeMillis());
        private static final String CRLF = CR + LF;
        private final StringBuilder logDetail = new StringBuilder();
        private final long requestId = requestIdGenerator.getAndIncrement();
        private Request request;
        private boolean enableRequestLog = true;
        private boolean enableResponseLog = true;
        private boolean ignoreSslVerify = false;
        private CookieStore cookieStore = new BasicCookieStore();

        private RequestBuilder(Request request) {
            this.request = Validate.notNull(request);
            logDetail.append(request.toString()).append(CRLF);
        }

        public RequestBuilder headers(Map<String, String> headerMap) {
            NullableStream.of(headerMap)
                .map(item -> new BasicHeader(item.getKey(), item.getValue()))
                .forEach(request::addHeader);

            logMap("add headers", headerMap);

            return this;
        }

        public RequestBuilder header(String key, String value) {
            if (isNoneBlank(key)) {
                if (isNull(value)) {
                    request.removeHeaders(key);
                } else {
                    request.addHeader(key, value);
                }
            }
            return this;
        }

//        public RequestBuilder tools.cookies(Iterable<Cookie> tools.cookies) {
//            tools.streamtools.NullableStream.of(tools.cookies)
//                .forEach(this::cookie);
//            return this;
//        }

//        public RequestBuilder cookie(String name, String value, String domain, String path) {
//            if (isNotBlank(name)) {
//                BasicClientCookie cookie = new BasicClientCookie(name, value);
//                cookie.setDomain(domain);
//                cookie.setPath(path);
//                cookie(cookie);
//            }
//            return this;
//        }
//
//        public RequestBuilder cookie(Cookie cookie) {
//            if (nonNull(cookie)) {
//                cookieStore.addCookie(toBasicClientCookie(cookie));
//            }
//            return this;
//        }
//
//        public RequestBuilder cookieStore(@NonNull CookieStore cookieStore) {
//            this.cookieStore = cookieStore;
//            return this;
//        }

        /**
         * 指客户端和服务进行数据交互的时间，是指两者之间如果两个数据包之间的时间大于该时间则认为超时，而不是整个交互的整体时间，
         * 比如如果设置1秒超时，如果每隔0.8秒传输一次数据，传输10次，总共8秒，这样是不超时的。而如果任意两个数据包之间的时间超过了1秒，则超时。
         */
        public RequestBuilder socketTimeout(Integer timeoutInMilliseconds) {
            if (isNull(timeoutInMilliseconds)) {
                timeoutInMilliseconds = -1;
            }
            request.socketTimeout(timeoutInMilliseconds);
            logDetail.append("socketTimeout=")
                .append(timeoutInMilliseconds)
                .append(CRLF);
            return this;
        }

        /**
         * 指建立连接的超时时间
         * 将此套接字连接到服务器，并指定一个超时值。在建立连接或者发生错误之前，连接一直处于阻塞状态。
         */
        public RequestBuilder connectTimeout(Integer timeoutInMilliseconds) {
            if (isNull(timeoutInMilliseconds)) {
                timeoutInMilliseconds = -1;
            }
            request.socketTimeout(timeoutInMilliseconds);
            logDetail.append("connectTimeout=")
                .append(timeoutInMilliseconds)
                .append(CRLF);
            return this;
        }

        private void logMap(String title, Map<String, String> map) {
            String headerString = NullableStream.of(map)
                .map(item -> format("%s -> %s", item.getKey(), item.getValue()))
                .collect(joining("\r\n"));
            if (isNotBlank(headerString)) {
                logDetail
                    .append(title)
                    .append(":")
                    .append(CRLF)
                    .append(headerString)
                    .append(CRLF);
            }
        }

        /**
         * socketTimeout and connectTimeout
         */
        public RequestBuilder timeout(Integer timeoutInMilliseconds) {
            return this
                .socketTimeout(timeoutInMilliseconds)
                .connectTimeout(timeoutInMilliseconds);
        }

        public RequestBuilder jsonBody(String body) {
            request = request.bodyString(body, APPLICATION_JSON);
            logDetail
                .append("jsonBody=")
                .append(body)
                .append(CRLF);
            return this;
        }

        public RequestBuilder streamBody(InputStream body) {
            request = request.bodyStream(body);
            logDetail
                .append("streamBody")
                .append(CRLF);
            return this;
        }

        public RequestBuilder textBody(String body) {
            request = request.bodyString(body, TEXT_PLAIN);
            logDetail
                .append("textBody=")
                .append(body)
                .append(CRLF);
            return this;
        }

//        public RequestBuilder jsonBody(Object body) {
//            return jsonBody(toJsonString(body));
//        }

        public RequestBuilder formBody(Map<String, String> formMap) {
            return formBody(formMap, UTF_8);
        }

        public RequestBuilder formBody(Map<String, String> formMap, Charset charset) {
            if (isNull(charset)) {
                charset = UTF_8;
            }
            Form form = Form.form();
            NullableStream.of(formMap)
                .forEach(f -> form.add(f.getKey(), f.getValue()));
            request = request.bodyForm(form.build(), charset);
            logMap("form body", formMap);
            return this;
        }

        public RequestBuilder formBody(Map<String, String> formMap, String charset) {
            Charset ch = UTF_8;
            if (isNotBlank(charset)) {
                ch = Charset.forName(charset);
            }
            return formBody(formMap, ch);
        }

        // todo, re-code
        public RequestBuilder fileBody(Map<String, byte[]> files) {

            MultipartEntityBuilder builder = MultipartEntityBuilder
                .create()
                .setMode(BROWSER_COMPATIBLE)
                .setCharset(UTF_8);
            NullableStream.of(files)
                .forEach(file -> builder.addBinaryBody("file", file.getValue(), MULTIPART_FORM_DATA, file.getKey()));

            HttpEntity entity = builder.build();
            request.body(entity);
            return this;
        }

        public RequestBuilder ignoreSslVerify(boolean ignore) {
            logDetail.append("ignoreSSl=")
                .append(ignore)
                .append(CRLF);
            this.ignoreSslVerify = ignore;
            return this;
        }

        @NotNull
        public ExecuteResult executeContent() {
            if (this.enableRequestLog) {
                log.debug("http request[{}] --> \r\n{}", requestId, logDetail.toString());
            }
            Content result = NO_CONTENT;
            try (CloseableHttpClient client = getClient()) {
                Executor executor = Executor.newInstance(client);
                result = executor.use(cookieStore)
                    .execute(request)
                    .returnContent();
            } catch (IOException e) {
                log.error("executeContent[" + requestId + "]:", e);
            }

            CookieStore cs = new ReadOnlyCookieStore(
                NullableStream.of(cookieStore.getCookies())
                    .map(ReadOnlyCookie::new)
                    .collect(toList())
            );
            return new ExecuteResult(result, cs);
        }

        @SneakyThrows
        private CloseableHttpClient getClient() {
            RequestConfig requestConfig = RequestConfig.custom()
                .build();

            HttpClientBuilder builder = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(requestConfig)
                .setRedirectStrategy(new LaxRedirectStrategy());

            if (this.ignoreSslVerify) {
                builder.setSSLHostnameVerifier(NoopSSLHelper.HOSTNAME_VERIFIER)
                    .setSSLContext(NoopSSLHelper.SSL_CONTEXT)
                    .setSSLSocketFactory(NoopSSLHelper.SOCKET_FACTORY);
            }
            return builder.build();
        }

        public String executeString() {
            String result = executeContent().asString(UTF_8);
            if (this.enableResponseLog) {
                log.debug("http response[{}] <-- \r\n{}", requestId, result);
            }
            return result;
        }
//
//        public String executeString(String jsonPath) {
//            String s = executeString();
//            return JsonPathUtils.readAsString(s, jsonPath);
//        }
//
//        public <T> T executeObject(String jsonPath, Class<T> klass) {
//            String s = executeString();
//            return JsonPathUtils.read(s, jsonPath, klass);
//        }

        public InputStream executeStream() {
            InputStream result = executeContent().asStream();
            if (this.enableResponseLog) {
                log.debug("http response (stream)[{}] <-- \r\n", requestId);
            }
            return result;
        }

        public byte[] executeByte() {
            return executeContent().asBytes();
        }

        public void execute() {
            executeContent();
        }

        public RequestBuilder disableRequestLog() {
            this.enableRequestLog = false;
            return this;
        }

        public RequestBuilder disableResponseLog() {
            this.enableResponseLog = false;
            return this;
        }

        public RequestBuilder disableLog() {
            disableRequestLog();
            disableResponseLog();
            return this;
        }

    }

    protected static class NoopSSLHelper {
        public static final SSLContext SSL_CONTEXT = createContext();
        public static final HostnameVerifier HOSTNAME_VERIFIER = NoopHostnameVerifier.INSTANCE;

        public static final LayeredConnectionSocketFactory SOCKET_FACTORY = new SSLConnectionSocketFactory(
            SSL_CONTEXT,
            new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"},
            null,
            HOSTNAME_VERIFIER
        );

        @SneakyThrows
        static SSLContext createContext() {
            return SSLContexts
                .custom()
                .loadTrustMaterial((chain, authType) -> true)
                .build();
        }

    }

    @Value
    public static class ExecuteResult {
        @NonNull
        @Delegate
        private Content content;

        @NonNull
        private CookieStore cookieStore;
    }

    //endregion


    public static <K, V> MultiValueMap<K, V> toMultiValueMap(@Nullable Map<K, V> map) {
        Map<K, List<V>> collect = NullableStream
            .of(map)
            .collect(
                groupingBy(
                    Map.Entry::getKey,
                    mapping(Map.Entry::getValue, toList())
                )
            );
        return new LinkedMultiValueMap<>(collect);
    }
}
